package org.easiermock;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.List;

import net.sf.cglib.core.CollectionUtils;
import net.sf.cglib.core.VisibilityPredicate;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.NoOp;

import org.easiermock.invocationhandlers.CglibInvocationHandlerAdapter;
import org.easiermock.invocationhandlers.UnsupportedOperationInvocationHandler;
import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;

/**
 * Provides factory methods for creating EasierMock mock objects. 
 * 
 * <p>Generated mocks consist of three main components:
 * <ul>
 *   <li>The class/interface to be mocked - referred to as the "real"</li>
 *   <li>The object generated by EasierMock - referred to as the mock</li>
 *   <li>The class that provides the mock behaviour - referred to as the delegate</li>
 * </ul>
 * 
 * <p>EasierMockCreates a mock object that implements/extends a real class or interface
 * and delegates through to a delegate object if it provides compatible methods. 
 * 
 * <p>Methods are considered compatible if they have the same signature 
 * <em>and</em> are annotated with the <code>Mock</code> annotation (either directly 
 * or if the class the method is declared on is annotated).
 * 
 * @author Neil Fuller
 */
public class EasierMock {
	
	private static Objenesis objenesis = new ObjenesisStd();
	
	private EasierMock() {
		super();
	}
	
	/**
	 * Constructs a mock using an abstract delegate class.
	 * 
	 * <p>Methods that are abstract on the delegate will throw UnsupportedOperationException
	 * if called on the resulting mock.
	 * 
	 * <p>The zero argument constructor will be invoked on the abstract class when the
	 * mock object is instantiated.
	 * 
	 * <p>An optional set of MethodListeners can be provided that will be 
	 * called prior to the calls to mocked methods.
	 * 
	 * @param <T> the class of the mock to create (usually inferred from the method arguments)
	 * @param abstractDelegateClass the delegate class that provides mock behaviour
	 * @param methodListeners the method listeners
	 * @return the mock instance
	 */
	public static <T> T createMock(Class<T> abstractDelegateClass, MethodListener ... methodListeners) {
		return createMock(abstractDelegateClass, null, null, methodListeners);
	}
	
	/**
	 * Constructs a mock using an abstract delegate class.
	 * 
	 * <p>Methods that are abstract on the delegate will throw UnsupportedOperationException
	 * if called on the resulting mock.
	 * 
	 * <p>The constructor with the specified arguments will be invoked on the abstract class 
	 * when the mock object is instantiated.
	 * 
	 * <p>An optional set of MethodListeners can be provided that will be 
	 * called prior to the calls to mocked methods.
	 * 
	 * @param <T> the class of the mock to create (usually inferred from the method arguments)
	 * @param abstractDelegateClass the delegate class that provides mock behaviour
	 * @param constructorTypes the types of the arguments on the constructor to call, null can be used in place of an empty array
	 * @param constructorArgs the arguments for the constructor to call, null can be used in place of an empty array
	 * @param methodListeners the method listeners
	 * @return the mock instance
	 */
	public static <T> T createMock(Class<? extends T> abstractDelegateClass, Class<?>[] constructorTypes, Object[] constructorArgs, MethodListener ... methodListeners) {
		T delegate = createConcreteInstance(abstractDelegateClass, constructorTypes, constructorArgs);
		return createMock(abstractDelegateClass, delegate, methodListeners);
	}
	
	/**
	 * Creates a mock object that implements/extends the specified class.
	 * 
	 * <p>An optional set of MethodListeners can be provided that will be 
	 * called prior to the calls to mocked methods.
	 * 
	 * @param <T> the class of the mock to create (usually inferred from the method arguments)
	 * @param realClass the class to mock
	 * @param delegate the mock implementation. Does not have to implement the <code>toMockClass</code> but the 
	 * 		signatures of the methods must be compatible to be  
	 * @param methodListeners the method listeners
	 * @return the mock instance
	 */
	public static <T> T createMock(Class<T> realClass, Object delegate, MethodListener ... methodListeners) {
		DefaultMockContext mockContext = new DefaultMockContext();
		for (MethodListener methodListener : methodListeners) {
			mockContext.addMethodListener(methodListener);
		}
		return createMock(realClass, delegate, mockContext);
	}
	
	static <T> T createMock(Class<T> realClass, Object delegate, MockContext mockContext) {
		DelegatingInvocationHandler invocationHandler = new DelegatingInvocationHandler(delegate, mockContext);
		return createMock(realClass, delegate, invocationHandler);
	}
	
	@SuppressWarnings("unchecked")
	private static <T> T createMock(Class<T> realClass, Object delegate, DelegatingInvocationHandler invocationHandler) {
		T mock;
		if (realClass.isInterface()) {
			ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
			mock = (T)Proxy.newProxyInstance(classLoader, new Class[] { realClass }, invocationHandler);
		} else {
			Callback interceptor = new CglibDelegatingInvocationHandler(invocationHandler);
			// BEGIN: CODE LARGELY COPIED FROM EASYMOCK
			
	        // Create the mock
	        Enhancer enhancer = new Enhancer() {
	            /**
	             * Filter all private constructors but do not check that there are
	             * some left
	             */
	        	@Override
	            protected void filterConstructors(Class sc, List constructors) {
	                CollectionUtils.filter(constructors, new VisibilityPredicate(
	                        sc, true));
	            }
	        };
	        enhancer.setSuperclass(realClass);
	        enhancer.setCallbackType(interceptor.getClass());

	        Class<?> mockClass = enhancer.createClass();
	        Enhancer.registerCallbacks(mockClass, new Callback[] { interceptor });

			// END: CODE LARGELY COPIED FROM EASYMOCK
	        
	        mock = (T)objenesis.newInstance(mockClass);
		}
		return mock;
	}
	
	/**
	 * Returns the delegate object for the supplied mock
	 * 
	 * @param mock the mock object
	 * @return the delegate
	 * @throws IllegalArgumentException if the object is not a mock
	 */
	public static Object getDelegate(Object mock) {
		DelegateHolder delegateHolder;
		if (mock instanceof Factory) {
			// CGLib proxy
			Factory mockAsFactory = (Factory)mock;
			Callback callback = mockAsFactory.getCallback(0);
			if ( !(callback instanceof DelegateHolder)) {
				throw new IllegalArgumentException("Object is not an EasierMock mock");
			}
			delegateHolder = (DelegateHolder)callback;
		} else if (Proxy.isProxyClass(mock.getClass())) {
			// Java dynamic proxy
			InvocationHandler invocationHandler = Proxy.getInvocationHandler(mock);
			if ( !(invocationHandler instanceof DelegateHolder)) {
				throw new IllegalArgumentException("Object is not an EasierMock mock");
			}
			delegateHolder = (DelegateHolder)invocationHandler;
		} else {
			throw new IllegalArgumentException("Object is not an EasierMock mock");
		}
		return delegateHolder.getDelegate();
	}
	
	/**
	 * Creates a concrete instance for an abstract class or interface. 
	 * 
	 * Any unimplemented methods of the class
	 * will throw UnsupportedOperationException when called on the instance. 
	 * 
	 * The zero-argument constructor
	 * will be called on the abstract class (if it exists, if not an exception will be thrown).
	 * 
	 * @param <T> the class of the mock to create (usually inferred from the method arguments)
	 * @param abstractClass the class to make concrete
	 * @return the mock instance
	 */
	public static <T> T createConcreteInstance(Class<T> abstractClass) {
		return createConcreteInstance(abstractClass, null, null);
	}
	
	/**
	 * Creates a concrete instance for an abstract class or interface. 
	 * 
	 * Any unimplemented methods of the class will cause the abstractMethodHandler to be invoked 
	 * when called on the returned instance.
	 * 
	 * The zero-argument constructor
	 * will be called on the abstract class (if it exists, if not an exception will be thrown).
	 * 
	 * @param <T> the class of the mock to create (usually inferred from the method arguments)
	 * @param abstractClass the class to make concrete
	 * @param abstractMethodHandler the InvocationHandler to invoke for abstract methods
	 * @return the mock instance
	 */
	public static <T> T createConcreteInstance(Class<T> abstractClass, InvocationHandler abstractMethodHandler) {
		return createConcreteInstance(abstractClass, abstractMethodHandler, null, null);
	}
	
	/**
	 * Creates a concrete instance for an abstract class or interface. 
	 * 
	 * <p>The constructor with the specified arguments will be invoked on the abstract class 
	 * when the mock object is instantiated.
	 * 
	 * <p>Any unimplemented methods of the class
	 * will throw UnsupportedOperationException when called on the instance. 
	 * 
	 * @param <T> the class of the mock to create (usually inferred from the method arguments)
	 * @param abstractClass the class to make concrete
	 * @param constructorTypes the types of the arguments on the constructor to call, null can be used in place of an empty array
	 * @param constructorArgs the arguments for the constructor to call, null can be used in place of an empty array
	 * @return the mock instance
	 */
	public static <T> T createConcreteInstance(Class<T> abstractClass, Class<?>[] constructorTypes, Object[] constructorArgs) {
		return createConcreteInstance(abstractClass, UnsupportedOperationInvocationHandler.INSTANCE, constructorTypes, constructorArgs);
	}
	
	/**
	 * Creates a concrete instance for an abstract class or interface. 
	 * 
	 * <p>The constructor with the specified arguments will be invoked on the abstract class 
	 * when the mock object is instantiated.
	 * 
	 * <p>Any unimplemented methods of the class will cause the abstractMethodHandler to be invoked 
	 * when called on the returned instance.
	 * 
	 * @param <T> the class of the mock to create (usually inferred from the method arguments)
	 * @param abstractClass the class to make concrete
	 * @param abstractMethodHandler the InvocationHandler to invoke for abstract methods
	 * @param constructorTypes the types of the arguments on the constructor to call, null can be used in place of an empty array
	 * @param constructorArgs the arguments for the constructor to call, null can be used in place of an empty array
	 * @return the mock instance
	 */
	@SuppressWarnings("unchecked")
	public static <T> T createConcreteInstance(Class<T> abstractClass, InvocationHandler abstractMethodHandler, Class<?>[] constructorTypes, Object[] constructorArgs) {
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(abstractClass);
		Callback[] callbacks = {NoOp.INSTANCE, new CglibInvocationHandlerAdapter(abstractMethodHandler) };
		enhancer.setCallbacks(callbacks);
		enhancer.setCallbackFilter(new CallbackFilter() {
			public int accept(Method method) {
				if (Modifier.isAbstract(method.getModifiers())) {
					return 1;
				} 
				return 0;
			}
		});
		
		if (constructorTypes == null) {
			constructorTypes = new Class[0];
		}
		if (constructorArgs == null) {
			constructorArgs = new Object[0];
		}
		
		return (T)enhancer.create(constructorTypes, constructorArgs);
	}
	
	private static class CglibDelegatingInvocationHandler implements net.sf.cglib.proxy.InvocationHandler, DelegateHolder {

		private DelegatingInvocationHandler javaInvocationHandler;
		
		public CglibDelegatingInvocationHandler(DelegatingInvocationHandler javaInvocationHandler) {
			this.javaInvocationHandler = javaInvocationHandler;
		}

		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {
			return javaInvocationHandler.invoke(proxy, method, args);
		}
		
		public Object getDelegate() {
			return javaInvocationHandler.getDelegate();
		}
		
	}
}
